language.ts ➔ setupLanguageToggle   A
last analyzed

Complexity

Conditions 4

Size

Total Lines 44
Code Lines 34

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 34
dl 0
loc 44
rs 9.064
c 0
b 0
f 0
1
import { getTranslation, type Translations } from '../../i18n';
2
import { elements } from '../../bip39';
3
import { loadWordlist } from '../../bip39';
4
import { updateDisplay } from '../../display';
5
import { saveLanguage, isLanguageActive, getUILanguageCode } from '../domain/languageHelpers';
6
7
export let currentTranslations: Translations = getTranslation('en');
8
export let currentLanguage = 'english';
9
10
const languageFlagsSVG: Record<string, string> = {
11
  english: `<svg class="flag-icon" width="28" height="28"><use href="/sprite.svg#flag-uk"/></svg>`,
12
  spanish: `<svg class="flag-icon" width="28" height="28"><use href="/sprite.svg#flag-es"/></svg>`,
13
  french: `<svg class="flag-icon" width="28" height="28"><use href="/sprite.svg#flag-fr"/></svg>`,
14
  italian: `<svg class="flag-icon" width="28" height="28"><use href="/sprite.svg#flag-it"/></svg>`,
15
  portuguese: `<svg class="flag-icon" width="28" height="28"><use href="/sprite.svg#flag-pt"/></svg>`,
16
  czech: `<svg class="flag-icon" width="28" height="28"><use href="/sprite.svg#flag-cz"/></svg>`,
17
  japanese: `<svg class="flag-icon" width="28" height="28"><use href="/sprite.svg#flag-jp"/></svg>`,
18
  korean: `<svg class="flag-icon" width="28" height="28"><use href="/sprite.svg#flag-kr"/></svg>`,
19
  chinese_simplified: `<svg class="flag-icon" width="28" height="28"><use href="/sprite.svg#flag-cn"/></svg>`,
20
  chinese_traditional: `<svg class="flag-icon" width="28" height="28"><use href="/sprite.svg#flag-tw"/></svg>`,
21
};
22
23
const browserLangToWordlist: Record<string, string> = {
24
  en: 'english',
25
  es: 'spanish',
26
  fr: 'french',
27
  it: 'italian',
28
  pt: 'portuguese',
29
  cs: 'czech',
30
  ja: 'japanese',
31
  ko: 'korean',
32
  'zh-Hans': 'chinese_simplified',
33
  'zh-CN': 'chinese_simplified',
34
  'zh-SG': 'chinese_simplified',
35
  'zh-Hant': 'chinese_traditional',
36
  'zh-TW': 'chinese_traditional',
37
  'zh-HK': 'chinese_traditional',
38
  'zh-MO': 'chinese_traditional',
39
  zh: 'chinese_simplified',
40
};
41
42
function getBrowserLanguage(): string {
43
  if (typeof navigator === 'undefined') {
44
    return 'english';
45
  }
46
47
  // Get browser language (e.g., "en-US", "es", "zh-CN")
48
  const browserLang = navigator.language || (navigator as { userLanguage?: string }).userLanguage;
49
50
  if (!browserLang) {
51
    return 'english';
52
  }
53
54
  // Try exact match first (e.g., "zh-CN")
55
  if (browserLangToWordlist[browserLang]) {
56
    return browserLangToWordlist[browserLang];
57
  }
58
59
  // Try language code only (e.g., "en" from "en-US")
60
  const langCode = browserLang.split('-')[0];
61
  if (browserLangToWordlist[langCode]) {
62
    return browserLangToWordlist[langCode];
63
  }
64
65
  return 'english';
66
}
67
68
export function initLanguage(): string {
69
  const savedLanguage = localStorage.getItem('language');
70
  const defaultLanguage = savedLanguage || getBrowserLanguage();
71
  currentLanguage = defaultLanguage;
72
73
  elements.currentFlag.innerHTML = languageFlagsSVG[defaultLanguage] || languageFlagsSVG['english'];
74
75
  updateActiveLanguageOption();
76
77
  return defaultLanguage;
78
}
79
80
export async function changeLanguage(newLanguage: string): Promise<void> {
81
  currentLanguage = newLanguage;
82
  saveLanguage(newLanguage);
83
84
  elements.currentFlag.innerHTML = languageFlagsSVG[newLanguage] || languageFlagsSVG['english'];
85
86
  const uiLang = getUILanguageCode(newLanguage);
87
  currentTranslations = getTranslation(uiLang);
88
89
  updateActiveLanguageOption();
90
91
  await loadWordlist(newLanguage);
92
  updateUITranslations();
93
}
94
95
function updateActiveLanguageOption(): void {
96
  const options = elements.languageDropdown.querySelectorAll('.language-option');
97
  options.forEach(option => {
98
    const btn = option as HTMLButtonElement;
99
    if (isLanguageActive(btn.dataset.lang || '', currentLanguage)) {
100
      btn.classList.add('active');
101
    } else {
102
      btn.classList.remove('active');
103
    }
104
  });
105
}
106
107
export function setupLanguageToggle(): void {
108
  let isOpen = false;
109
110
  // Toggle dropdown
111
  elements.languageToggle.addEventListener('click', e => {
112
    e.stopPropagation();
113
    isOpen = !isOpen;
114
    elements.languageDropdown.classList.toggle('open', isOpen);
115
    elements.languageToggle.setAttribute('aria-expanded', isOpen.toString());
116
  });
117
118
  // Close dropdown when clicking outside
119
  document.addEventListener('click', e => {
120
    if (isOpen && !elements.languageDropdown.contains(e.target as Node)) {
121
      isOpen = false;
122
      elements.languageDropdown.classList.remove('open');
123
      elements.languageToggle.setAttribute('aria-expanded', 'false');
124
    }
125
  });
126
127
  // Handle language option clicks
128
  const options = elements.languageDropdown.querySelectorAll('.language-option');
129
  options.forEach(option => {
130
    option.addEventListener('click', () => {
131
      const btn = option as HTMLButtonElement;
132
      const lang = btn.dataset.lang;
133
      if (lang) {
134
        void changeLanguage(lang).then(() => {
135
          // Close dropdown
136
          isOpen = false;
137
          elements.languageDropdown.classList.remove('open');
138
          elements.languageToggle.setAttribute('aria-expanded', 'false');
139
        });
140
      }
141
    });
142
  });
143
144
  elements.languageToggle.addEventListener('keydown', e => {
145
    if (e.key === 'Escape' && isOpen) {
146
      isOpen = false;
147
      elements.languageDropdown.classList.remove('open');
148
      elements.languageToggle.setAttribute('aria-expanded', 'false');
149
      elements.languageToggle.focus();
150
    }
151
  });
152
}
153
154
export function setTranslations(translations: Translations): void {
155
  currentTranslations = translations;
156
}
157
158
function updateBasicUITranslations(): void {
159
  elements.title.textContent = currentTranslations.title;
160
  elements.indexLabel.textContent = currentTranslations.index;
161
  elements.resetButton.textContent = currentTranslations.resetButton;
162
  elements.infoText.textContent = currentTranslations.infoText;
163
  elements.privacyTitle.textContent = currentTranslations.privacyTitle;
164
  elements.privacyText.textContent = currentTranslations.privacyTooltip;
165
  elements.themeToggle.title = currentTranslations.toggleTheme;
166
  elements.languageToggle.title = currentTranslations.languageLabel;
167
  elements.helpIconTitle.textContent = currentTranslations.helpIconLabel;
168
}
169
170
function updateWordInputTranslations(): void {
171
  elements.wordInput.placeholder = currentTranslations.wordInputPlaceholder;
172
}
173
174
function updateModalTranslations(): void {
175
  elements.modalTitle.textContent = currentTranslations.modalTitle;
176
177
  updateModalStep1Translations();
178
  updateModalStep2Translations();
179
  updateModalStep3Translations();
180
  updateModalStep4Translations();
181
  updateModalWarningTranslations();
182
  updateModalWhyTranslations();
183
}
184
185
function updateModalStep1Translations(): void {
186
  elements.modalStep1Title.textContent = currentTranslations.modalStep1Title;
187
  elements.modalStep1Text.textContent = currentTranslations.modalStep1Text;
188
189
  elements.modalStep1WordGrid.innerHTML = '';
190
  currentTranslations.modalStep1Words.forEach(word => {
191
    const wordSpan = document.createElement('span');
192
    wordSpan.className = 'word-example';
193
    wordSpan.textContent = word;
194
    elements.modalStep1WordGrid.appendChild(wordSpan);
195
  });
196
}
197
198
function updateModalStep2Translations(): void {
199
  elements.modalStep2Title.textContent = currentTranslations.modalStep2Title;
200
  elements.modalStep2Text.textContent = currentTranslations.modalStep2Text;
201
  elements.modalStep2Word1.textContent = currentTranslations.modalStep1Words[0];
202
  elements.modalStep2Word2.textContent = currentTranslations.modalStep1Words[1];
203
  elements.modalStep2Entropy.textContent = currentTranslations.modalStep2Entropy;
204
}
205
206
function updateModalStep3Translations(): void {
207
  elements.modalStep3Title.textContent = currentTranslations.modalStep3Title;
208
  elements.modalStep3Text.textContent = currentTranslations.modalStep3Text;
209
  elements.modalStep3MasterSeed.textContent = currentTranslations.modalStep3MasterSeed;
210
  elements.modalStep3BitValue.textContent = currentTranslations.modalStep3BitValue;
211
}
212
213
function updateModalStep4Translations(): void {
214
  elements.modalStep4Title.textContent = currentTranslations.modalStep4Title;
215
  elements.modalStep4Text.textContent = currentTranslations.modalStep4Text;
216
  elements.modalStep4PrivateKey.textContent = currentTranslations.modalStep4PrivateKey;
217
  elements.modalStep4PrivateKey1.textContent = currentTranslations.modalStep4PrivateKey1;
218
  elements.modalStep4PrivateKey2.textContent = currentTranslations.modalStep4PrivateKey2;
219
  elements.modalStep4PrivateKey3.textContent = currentTranslations.modalStep4PrivateKey3;
220
  elements.modalStep4BitSize1.textContent = currentTranslations.modalStep4BitSize;
221
  elements.modalStep4BitSize2.textContent = currentTranslations.modalStep4BitSize;
222
  elements.modalStep4BitSize3.textContent = currentTranslations.modalStep4BitSize;
223
  elements.modalStep4PublicKey.textContent = currentTranslations.modalStep4PublicKey;
224
  elements.modalStep4Address.textContent = currentTranslations.modalStep4Address;
225
}
226
227
function updateModalWarningTranslations(): void {
228
  elements.modalWarningTitle.textContent = currentTranslations.modalWarningTitle;
229
  elements.modalWarningText.textContent = currentTranslations.modalWarningText;
230
  elements.modalWarningItem1.textContent = currentTranslations.modalWarningItem1;
231
  elements.modalWarningItem2.textContent = currentTranslations.modalWarningItem2;
232
  elements.modalWarningItem3.textContent = currentTranslations.modalWarningItem3;
233
  elements.modalWarningItem4.textContent = currentTranslations.modalWarningItem4;
234
}
235
236
function updateModalWhyTranslations(): void {
237
  elements.modalWhyTitle.textContent = currentTranslations.modalWhyBIP39Title;
238
  elements.modalWhyText.textContent = currentTranslations.modalWhyBIP39Text;
239
240
  elements.modalWhyLink.innerHTML = `
241
    <svg width="18" height="18">
242
      <use href="/sprite.svg#icon-lightbulb"/>
243
    </svg>
244
    ${currentTranslations.modalWhyBIP39Link}
245
  `;
246
}
247
248
export function updateUITranslations(): void {
249
  updateBasicUITranslations();
250
  updateWordInputTranslations();
251
  updateModalTranslations();
252
253
  updateDisplay();
254
}
255